Skip to content

S00-00 General-Cloudflare-D1、R2、KV

[TOC]

D1

D1(边缘关系型数据库):

你想全面了解 Cloudflare D1 数据库,包括它的定位、核心特性、使用流程、与 Workers/Hono 的结合方式,以及实战中的常用操作——接下来我会从「基础认知→核心优势→实操流程→实战示例→最佳实践」层层拆解,重点聚焦 D1 在边缘开发场景的实际应用,确保内容通俗易懂且能直接落地。

什么是 D1

什么是 Cloudflare D1?:

Cloudflare D1 是 Cloudflare 推出的开源、SQLite 兼容的边缘关系型数据库,专为 Cloudflare 全球边缘网络设计,核心定位是解决「边缘应用的结构化数据存储与复杂查询」需求。

核心背景与定位:

  • 填补生态空白:Cloudflare 原有存储方案(KV 键值、R2 对象、Durable Objects 状态)均不适合结构化、关联型数据(如用户信息、订单、商品列表),D1 则专门适配这类场景;
  • 边缘原生:数据部署在 Cloudflare 全球 300+ 数据中心,就近响应请求,延迟低至毫秒级;
  • 低学习成本:完全兼容 SQLite 语法(最流行的轻量 SQL 数据库),无需学习新的 SQL 方言;
  • 无缝衔接 Workers/Pages:与 Cloudflare 边缘运行时深度整合,代码中可直接通过 env 访问,无额外网络请求开销。

简单来说:D1 是 Cloudflare 边缘应用的「MySQL/PostgreSQL 平替」,轻量、低延迟、适配边缘场景,适合开发需要 SQL 能力的边缘应用(如用户系统、内容管理、订单管理)。

D1 核心优势

D1 核心优势(对比传统数据库/Cloudflare 其他存储):

特性维度Cloudflare D1传统云数据库(MySQL/PostgreSQL)Cloudflare KV(键值存储)
部署位置全球边缘节点中心化集群全球边缘节点
访问延迟极低(就近响应)较高(需跨网络)极低
SQL 支持完整 SQLite 语法完整 SQL 语法无(仅键值查询)
关联查询支持(JOIN/子查询)支持不支持
事务支持 ACID 特性支持不支持
学习成本低(SQLite 语法)中(不同方言差异)极低
适用场景边缘结构化数据、复杂查询中心化大规模数据存储简单键值、高频读写

D1 核心能力总结:

  1. SQLite 100% 兼容:支持 SELECT/INSERT/UPDATE/DELETE、JOIN、子查询、索引、事务等核心 SQL 能力;
  2. 边缘低延迟:数据就近存储,全球用户访问延迟比中心化数据库低一个数量级;
  3. Workers 无缝集成:代码中直接通过 c.env 调用,无需配置连接字符串、处理网络超时;
  4. 免费额度友好:开发测试阶段完全免费(1GB 存储、100 万次查询/月),满足小型应用需求;
  5. 开发体验佳:支持 Wrangler 命令行管理、交互式 SQL 终端、数据导入/导出。

D1 前置准备

D1 前置准备:

使用 D1 前需完成基础环境配置,确保后续操作顺畅:

  1. Wrangler 版本要求:需安装 v3.0 及以上版本(D1 核心功能在 v3 中完善):
    bash
    # 安装/升级 Wrangler
    npm install -g wrangler
    # 验证版本
    wrangler -v # 输出 >= 3.0.0 即可
  2. 登录 Cloudflare 账户
    bash
    wrangler login # 自动打开浏览器授权
  3. 基础 SQL 知识:了解 SQLite 基础语法(建表、增删改查),无需深入进阶特性。

D1 基础 Wrangler 命令行操作

D1 的数据库管理主要通过 Wrangler 命令完成,以下是高频使用的命令:

创建 D1 数据库

创建 D1 数据库:

bash
# 交互式创建(推荐,自动生成数据库名称)
wrangler d1 create <DB_NAME> # 如 wrangler d1 create my-first-d1-db

# 非交互式创建(指定区域)
wrangler d1 create <DB_NAME> --location eu # 区域可选:us/eu/apac 等
  • 执行成功后,终端会输出数据库 ID绑定配置代码(需复制到 wrangler.toml,核心!);
  • 示例输出:
    ✨ Successfully created DB 'my-first-d1-db' (ID: 12345678-1234-1234-1234-1234567890ab)
    ⚡️ Add the following to your wrangler.toml:
    [[d1_databases]]
    binding = "MY_DB" # 代码中通过 env.MY_DB 访问
    database_name = "my-first-d1-db"
    database_id = "12345678-1234-1234-1234-1234567890ab"

查看数据库列表

查看数据库列表:

bash
wrangler d1 list
  • 输出账户下所有 D1 数据库的名称、ID、状态,方便核对数据库是否创建成功。

执行 SQL 文件

执行 SQL 文件(初始化表结构):

bash
# 执行本地 SQL 文件(如 schema.sql)
wrangler d1 execute <DB_NAME> --file=./schema.sql

# 示例:执行建表 SQL
# schema.sql 内容:
# CREATE TABLE IF NOT EXISTS users (
#   id INTEGER PRIMARY KEY AUTOINCREMENT,
#   name TEXT NOT NULL,
#   email TEXT UNIQUE NOT NULL,
#   created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
# );
wrangler d1 execute my-first-d1-db --file=./schema.sql

交互式 SQL 终端

交互式 SQL 终端(调试/临时操作):

bash
wrangler d1 execute <DB_NAME> --interactive
  • 进入 SQLite 风格的交互式终端,可直接输入 SQL 语句执行(如 SELECT * FROM users;);
  • 适合调试表结构、临时插入测试数据、验证查询语句。

执行单条 SQL 语句

执行单条 SQL 语句(快速测试):

bash
# 执行插入语句
wrangler d1 execute my-first-d1-db --command="INSERT INTO users (name, email) VALUES ('Cloudflare', 'test@cf.com')"

# 执行查询语句
wrangler d1 execute my-first-d1-db --command="SELECT * FROM users;"

绑定 D1 到 Workers 项目

绑定 D1 到 Workers 项目:

将创建数据库时输出的配置添加到项目的 wrangler.toml 中,才能在代码中访问 D1:

toml
# wrangler.toml 核心配置
name = "d1-hono-demo"
main = "src/index.ts"
compatibility_date = "2026-01-24"

# D1 数据库绑定(关键!)
[[d1_databases]]
binding = "MY_DB" # 代码中通过 c.env.MY_DB 访问
database_name = "my-first-d1-db"
database_id = "12345678-1234-1234-1234-1234567890ab" # 替换为你的数据库 ID

实战:D1 结合 Hono 开发边缘应用

以下是完整的「Hono + D1」开发流程,实现用户信息的 CRUD 操作,可直接复制使用:

步骤 1:初始化 Hono 项目:

bash
# 初始化项目
wrangler init d1-hono-demo
cd d1-hono-demo
# 安装 Hono
npm install hono

步骤 2:配置 wrangler.toml:

按上文要求,添加 D1 绑定配置(替换为你的数据库 ID/名称)。

步骤 3:编写 D1 表结构(schema.sql):

在项目根目录创建 schema.sql

sql
-- 创建用户表
CREATE TABLE IF NOT EXISTS users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  email TEXT UNIQUE NOT NULL,
  age INTEGER,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 插入测试数据
INSERT OR IGNORE INTO users (name, email, age) VALUES
('Alice', 'alice@cf.com', 25),
('Bob', 'bob@cf.com', 30);

步骤 4:执行 SQL 初始化表结构:

bash
wrangler d1 execute my-first-d1-db --file=./schema.sql

步骤 5:编写 Hono + D1 核心代码(src/index.ts):

typescript
import { Hono } from 'hono'

// 创建 Hono 实例
const app = new Hono()

// 1. 查询所有用户
app.get('/users', async (c) => {
  // 通过 c.env.MY_DB 访问 D1 数据库
  const { results } = await c.env.MY_DB.prepare('SELECT * FROM users ORDER BY created_at DESC').all() // all() 返回所有结果,first() 返回第一条,run() 执行无返回值

  return c.json({
    code: 200,
    data: results
  })
})

// 2. 根据 ID 查询单个用户
app.get('/users/:id', async (c) => {
  const id = c.req.param('id')
  // 使用参数绑定(防止 SQL 注入,核心!)
  const { results } = await c.env.MY_DB.prepare('SELECT * FROM users WHERE id = ?').bind(id).all()

  if (results.length === 0) {
    return c.json({ code: 404, msg: 'User not found' }, { status: 404 })
  }

  return c.json({
    code: 200,
    data: results[0]
  })
})

// 3. 创建新用户(POST 请求)
app.post('/users', async (c) => {
  try {
    const { name, email, age } = await c.req.json()
    // 验证参数
    if (!name || !email) {
      return c.json({ code: 400, msg: 'Name and email are required' }, { status: 400 })
    }

    // 插入数据
    const result = await c.env.MY_DB.prepare('INSERT INTO users (name, email, age) VALUES (?, ?, ?)')
      .bind(name, email, age)
      .run()

    return c.json(
      {
        code: 201,
        msg: 'User created',
        data: { id: result.meta.last_row_id }
      },
      { status: 201 }
    )
  } catch (err) {
    // 捕获唯一键冲突(email 重复)
    if (err.message.includes('UNIQUE constraint failed')) {
      return c.json({ code: 409, msg: 'Email already exists' }, { status: 409 })
    }
    return c.json({ code: 500, msg: 'Server error' }, { status: 500 })
  }
})

// 4. 更新用户信息
app.put('/users/:id', async (c) => {
  const id = c.req.param('id')
  const { name, age } = await c.req.json()

  const result = await c.env.MY_DB.prepare('UPDATE users SET name = ?, age = ? WHERE id = ?').bind(name, age, id).run()

  if (result.meta.changes === 0) {
    return c.json({ code: 404, msg: 'User not found' }, { status: 404 })
  }

  return c.json({ code: 200, msg: 'User updated' })
})

// 5. 删除用户
app.delete('/users/:id', async (c) => {
  const id = c.req.param('id')
  const result = await c.env.MY_DB.prepare('DELETE FROM users WHERE id = ?').bind(id).run()

  if (result.meta.changes === 0) {
    return c.json({ code: 404, msg: 'User not found' }, { status: 404 })
  }

  return c.json({ code: 200, msg: 'User deleted' })
})

// 导出 Workers 处理函数
export default app.fetch

步骤 6:本地调试与部署:

bash
# 本地调试(启动开发服务器)
wrangler dev

# 部署到 Cloudflare 线上
wrangler deploy

测试接口(示例):

  • 访问 http://localhost:8787/users → 查看所有用户;
  • POST 请求 http://localhost:8787/users,Body 传 {"name":"Charlie","email":"charlie@cf.com","age":28} → 创建用户;
  • PUT 请求 http://localhost:8787/users/3,Body 传 {"name":"Charlie Updated","age":29} → 更新用户;
  • DELETE 请求 http://localhost:8787/users/3 → 删除用户。

D1 进阶技巧与注意事项

防止 SQL 注入

防止 SQL 注入(核心!):

  • 必须使用 bind() 方法绑定参数,而非直接拼接 SQL 字符串(如 SELECT * FROM users WHERE id = ${id} 会导致注入);
  • 示例:prepare('SELECT * FROM users WHERE id = ?').bind(id) 是安全写法。

事务支持

事务支持:

D1 支持 ACID 事务,适合多步操作(如订单创建+库存扣减):

typescript
app.post('/transaction-demo', async (c) => {
  // 开启事务
  const tx = c.env.MY_DB.transaction()
  try {
    // 执行多个操作
    tx.prepare('INSERT INTO users (name, email) VALUES (?, ?)').bind('Dave', 'dave@cf.com')
    tx.prepare('UPDATE users SET age = ? WHERE email = ?').bind(35, 'dave@cf.com')

    // 提交事务
    await tx.commit()
    return c.json({ msg: 'Transaction success' })
  } catch (err) {
    // 回滚事务
    await tx.rollback()
    return c.json({ msg: 'Transaction failed', error: err.message }, { status: 500 })
  }
})

索引优化

索引优化:

对高频查询的字段创建索引,提升查询性能:

sql
-- 为 email 字段创建索引(加速按 email 查询)
CREATE INDEX IF NOT EXISTS idx_users_email ON users (email);
-- 为 age 字段创建索引(加速按 age 筛选)
CREATE INDEX IF NOT EXISTS idx_users_age ON users (age);

配额与限制

配额与限制(免费版):

  • 存储上限:1GB;
  • 每月查询次数:100 万次;
  • 单条 SQL 执行超时:5 秒;
  • 不支持异地容灾(需手动备份)。

数据备份与导出

数据备份与导出:

bash
# 导出 D1 数据为 SQL 文件
wrangler d1 execute <DB_NAME> --command=".dump" > backup.sql

# 导入备份数据
wrangler d1 execute <DB_NAME> --file=backup.sql

D1 常用属性与方法

Cloudflare D1 常用属性与方法详细解析:

Cloudflare D1 的所有操作均基于Wrangler 绑定后的 D1 数据库实例(如 Hono 中通过 c.env.MY_DB 访问的实例,以下简称D1 实例),同时执行 SQL 后返回的结果对象包含核心属性(如查询结果、执行元数据),这两类是开发中最常用的核心对象。

本次将以D1 实例的常用方法(SQL 执行、事务、批量操作)+执行结果对象的常用属性(核心返回值)为核心,结合 Cloudflare Workers/Hono 实际开发场景,逐一讲解语法、作用、使用示例,所有内容均为边缘开发高频用法,可直接落地。

核心前提:

  1. D1 实例需先在 wrangler.toml 中配置绑定([[d1_databases]]),才能在代码中通过 c.env.<BINDING_NAME> 访问;
  2. D1 完全兼容 SQLite 语法,所有方法均围绕SQL 预编译、参数绑定、执行设计,必须使用参数绑定(bind)防止 SQL 注入,这是 D1 开发的核心规范;
  3. 所有 D1 操作均为异步方法,需配合 async/await 使用。

D1 实例的核心常用方法:

D1 实例(如 c.env.MY_DB)是操作数据库的唯一入口,方法分为基础 SQL 执行事务处理批量操作三大类,其中 prepare 是所有 SQL 操作的基础(预编译 SQL 语句)。

模块 1:基础 SQL 执行

所有 SQL 操作的统一流程:prepare(SQL语句)bind(参数)执行方法(all/first/get/run),核心是通过预编译+参数绑定实现安全的 SQL 执行。

prepare()

prepare(sql: string) - SQL 预编译(基础方法):

  • 语法d1Instance.prepare(sql)
  • 核心作用:接收原始 SQL 语句(含占位符 ?),创建预编译语句对象,后续可通过 bind 绑定参数、调用执行方法(all/first 等),是 D1 所有 SQL 操作的前置步骤
  • 关键:SQL 语句中用问号 ? 作为参数占位符,不可直接拼接变量(防止注入)。
  • 示例
    typescript
    // 创建预编译语句对象,? 是参数占位符
    const stmt = c.env.MY_DB.prepare('SELECT * FROM users WHERE id = ?')

bind()

bind(...params: any[]) - 参数绑定(配合 prepare 使用):

  • 语法prepare(sql).bind(p1, p2, ...pn)
  • 核心作用:为预编译语句的 ? 占位符按顺序绑定参数,支持任意类型(字符串、数字、布尔、null),绑定后返回可执行的语句对象,可直接调用执行方法。
  • 关键:参数数量、类型需与 SQL 中的 ? 一一对应。
  • 示例
    typescript
    // 单个参数绑定
    c.env.MY_DB.prepare('SELECT * FROM users WHERE id = ?').bind(1)
    // 多个参数绑定(按顺序对应 ?)
    c.env.MY_DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)').bind('Alice', 'alice@cf.com')

all()

all() - 执行查询并返回所有结果(查多条):

  • 语法prepare(sql).bind(...params).all()

  • 核心作用:执行 SQL 语句(主要用于 SELECT),返回符合条件的所有数据,是查询多条记录的首选方法。

  • 返回值{results,meta},包含 results(数组,所有查询结果)和 meta(执行元数据)的对象。

  • Cloudflare+Hono 示例(查询所有用户/多条件查询):

    typescript
    // 查所有用户
    app.get('/users', async (c) => {
      const res = await c.env.MY_DB.prepare('SELECT * FROM users ORDER BY created_at DESC').all() // 无参数可直接调用 all()
      return c.json({ data: res.results, meta: res.meta })
    })
    
    // 多参数查询(按年龄和名称模糊查询)
    app.get('/users/filter', async (c) => {
      const age = c.req.query('age') || 0
      const name = c.req.query('name') || ''
      const res = await c.env.MY_DB.prepare('SELECT * FROM users WHERE age > ? AND name LIKE ?')
        .bind(age, `%${name}%`)
        .all()
      return c.json({ count: res.results.length, data: res.results })
    })

first()

first([colName?: string]) - 执行查询并返回第一条结果(查单条):

  • 语法prepare(sql).bind(...params).first() / first(colName)

  • 核心作用:执行 SQL 语句,仅返回第一条查询结果,适合单条记录查询(如按 ID 查用户);若传入列名,可直接返回该列的单个值

  • 返回值

    • 无列名:返回第一条结果的对象(无结果则为 undefined);
    • 有列名:返回第一条结果中该列的单个值(无结果则为 undefined)。
  • Cloudflare+Hono 示例(按 ID 查用户/直接获取单个列值):

    typescript
    // 按 ID 查单个用户(返回完整对象)
    app.get('/users/:id', async (c) => {
      const res = await c.env.MY_DB.prepare('SELECT * FROM users WHERE id = ?').bind(c.req.param('id')).first()
      if (!res) return c.json({ msg: 'User not found' }, { status: 404 })
      return c.json({ data: res })
    })
    
    // 直接获取单个列值(如获取用户名称)
    app.get('/users/:id/name', async (c) => {
      const userName = await c.env.MY_DB.prepare('SELECT name FROM users WHERE id = ?')
        .bind(c.req.param('id'))
        .first('name') // 直接返回 name 列的值
      return c.json({ name: userName || 'Unknown' })
    })

get()

get([colName?: string]) - 执行查询并返回单个值(查单个字段):

  • 语法prepare(sql).bind(...params).get() / get(colName)

  • 核心作用:专为获取单个值设计(如计数、查单个字段),比 first 更轻量,若查询结果有多行/多列,仅返回第一行第一列的值。

  • 返回值:单个原始值(字符串/数字/布尔/null),无结果则为 undefined

  • Cloudflare+Hono 示例(统计用户总数/查单个字段):

    typescript
    // 统计用户总数(核心用法)
    app.get('/users/count', async (c) => {
      const total = await c.env.MY_DB.prepare('SELECT COUNT(*) FROM users').get() // 直接返回计数结果(数字)
      return c.json({ total_users: total })
    })
    
    // 查单个字段(等价于 first('email'),更简洁)
    app.get('/users/:id/email', async (c) => {
      const email = await c.env.MY_DB.prepare('SELECT email FROM users WHERE id = ?').bind(c.req.param('id')).get('email')
      return c.json({ email: email || 'Unknown' })
    })

run()

run() - 执行无返回结果的 SQL(增/删/改):

  • 语法prepare(sql).bind(...params).run()

  • 核心作用:执行非查询类 SQLINSERT/UPDATE/DELETE/CREATE TABLE 等),无查询结果返回,仅返回执行元数据(如受影响行数、自增 ID),是增删改的首选方法。

  • 返回值{meta},仅包含 meta 执行元数据的对象(无 results)。

  • Cloudflare+Hono 示例(创建用户/更新用户/删除用户):

    typescript
    // 新增用户(INSERT)
    app.post('/users', async (c) => {
      const { name, email } = await c.req.json()
      const res = await c.env.MY_DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)').bind(name, email).run()
      // res.meta 包含自增 ID 和受影响行数
      return c.json({ msg: 'User created', user_id: res.meta.last_row_id }, { status: 201 })
    })
    
    // 更新用户(UPDATE)
    app.put('/users/:id', async (c) => {
      const { name } = await c.req.json()
      const res = await c.env.MY_DB.prepare('UPDATE users SET name = ? WHERE id = ?').bind(name, c.req.param('id')).run()
      // res.meta.changes 是受影响的行数,判断是否更新成功
      if (res.meta.changes === 0) return c.json({ msg: 'User not found' }, { status: 404 })
      return c.json({ msg: 'User updated' })
    })
    
    // 删除用户(DELETE)
    app.delete('/users/:id', async (c) => {
      const res = await c.env.MY_DB.prepare('DELETE FROM users WHERE id = ?').bind(c.req.param('id')).run()
      if (res.meta.changes === 0) return c.json({ msg: 'User not found' }, { status: 404 })
      return c.json({ msg: 'User deleted' })
    })

模块 2:事务处理方法

D1 支持完整的 ACID 事务,适合需要多步 SQL 操作原子性的场景(如「创建订单+扣减库存」,要么都成功,要么都失败),核心是 transaction() 方法创建事务对象,配合 commit()/rollback() 完成提交/回滚。

transaction()

transaction() - 创建事务对象:

  • 语法const tx = d1Instance.transaction()
  • 核心作用:创建 D1 事务对象,后续可在该对象上调用 prepare/bind 注册多步 SQL 操作,事务内的操作默认不会立即执行,需调用 commit() 提交。
  • 关键:事务对象的 SQL 注册方式与 D1 实例完全一致。

commit()

commit() - 提交事务:

  • 语法await tx.commit()
  • 核心作用:执行事务对象中注册的所有 SQL 操作,若全部执行成功,事务提交,数据持久化;若任意一步失败,抛出异常,需配合 try/catch 调用 rollback()

rollback()

rollback() - 回滚事务:

  • 语法await tx.rollback()
  • 核心作用:当事务内任意一步操作失败时,回滚所有已注册的 SQL 操作,恢复到事务执行前的数据库状态,保证数据一致性。

示例:Cloudflare+Hono 事务

Cloudflare+Hono 事务完整示例(创建用户+新增用户日志):

typescript
// 先创建用户日志表(提前执行 SQL)
// CREATE TABLE IF NOT EXISTS user_logs (
//   id INTEGER PRIMARY KEY AUTOINCREMENT,
//   user_id INTEGER NOT NULL,
//   operation TEXT NOT NULL,
//   created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
//   FOREIGN KEY (user_id) REFERENCES users (id)
// );

app.post('/users/with-log', async (c) => {
  const { name, email } = await c.req.json()
  // 1. 创建事务对象
  const tx = c.env.MY_DB.transaction()

  try {
    // 2. 向事务中注册多步 SQL 操作(仅注册,不执行)
    // 步骤1:新增用户
    tx.prepare('INSERT INTO users (name, email) VALUES (?, ?)').bind(name, email)
    // 步骤2:新增用户日志(获取刚新增的自增 ID,用 last_insert_rowid())
    tx.prepare('INSERT INTO user_logs (user_id, operation) VALUES (last_insert_rowid(), ?)').bind('create')

    // 3. 提交事务:执行所有注册的操作
    const res = await tx.commit()
    return c.json(
      {
        msg: 'User created with log',
        user_id: res.meta.last_row_id
      },
      { status: 201 }
    )
  } catch (err) {
    // 4. 回滚事务:任意一步失败,恢复数据
    await tx.rollback()
    return c.json(
      {
        msg: 'Operation failed',
        error: err.message
      },
      { status: 500 }
    )
  }
})

模块 3:批量操作方法

batch 适合批量执行相同结构的 SQL 语句(如批量插入多条数据),比循环调用 run() 更高效(减少数据库交互次数),核心是 prepare 预编译一次,传入多组参数批量执行。

batch()

batch(paramsArray: any[][]) - 批量执行 SQL:

  • 语法prepare(sql).batch(paramsArray)

  • 核心作用:对预编译的 SQL 语句,传入二维参数数组,批量执行多组操作,每组参数对应 SQL 中的 ? 占位符。

  • 参数paramsArray 为二维数组,如 [[p1,p2], [p3,p4], ...],每组子数组对应一次 SQL 执行的参数。

  • 返回值:包含每组执行结果的数组,每个结果均为含 meta 的对象。

  • Cloudflare+Hono 批量操作示例(批量插入用户):

    ts
    tagsApp.post('/batch', async (c) => {
      // 1. 获取参数
      const tags: Tag[] = await c.req.json()
      if (!Array.isArray(tags) || tags.length === 0) return c.json({ error: 'At least one tag is required' }, 400)
    
      // 2. 组织 SQL 语句
      try {
        const keys = Object.keys(tags[0])
        const fieldsStr = keys.join(',')
        const valuesStr = keys.map(() => '?').join(',')
        let stmt = `INSERT INTO tags (${fieldsStr}) VALUES (${valuesStr})`
        const stmts = tags.map((tag) => c.env.DB_JAV.prepare(stmt).bind(...Object.values(tag)))
    
        // 3. 执行数据库操作
        const results = await c.env.DB_JAV.batch(stmts)
    
        // 4. 返回结果
        return c.json({
          success: results.every((result) => result.success),
          results: results
        })
      } catch (error) {
        return c.json({ error: error instanceof Error ? error.message : error }, 500)
      }
    })

特性:batch() 默认开启隐式事务(Implicit Transaction)

  1. 会回滚吗? 会。 batch() 默认在 隐式事务(Implicit Transaction) 中执行。如果其中任何一条语句失败,所有语句(包括之前执行成功的)都会回滚,数据库状态会恢复到执行 batch() 之前的状态。

  2. 后续还会执行吗? 不会。 一旦遇到错误,执行立即由于异常而中断,后续尚未执行的语句会被跳过。

详细机制解析:

Cloudflare D1 的 batch() 方法不仅仅是为了减少 HTTP 请求往返(Round-trip)的性能优化,它也是 D1 实现 原子性(Atomicity) 的主要手段。

当你调用 env.DB.batch([stmt1, stmt2, stmt3]) 时,D1 的处理逻辑如下:

  1. 开启事务:D1 会自动执行 BEGIN TRANSACTION

  2. 顺序执行:按照数组顺序依次执行 SQL 语句。

  3. 错误捕获

    • 如果 stmt1 成功,继续执行 stmt2
    • 如果 stmt2 失败
    • D1 捕获错误。
    • 执行 ROLLBACK(回滚 stmt1 的操作)。
    • 向你的 Worker 抛出异常(Error)。
    • stmt3 永远不会被执行
  4. 提交事务:只有当数组中所有语句都成功执行后,D1 才会执行 COMMIT,将变更持久化。

代码示例:

假设你正在使用 Hono 或原生 Worker 开发:

javascript
try {
  // 准备三个语句
  const stmt1 = env.DB.prepare('INSERT INTO users (name) VALUES (?)').bind('Alice')

  // 假设这是一个故意写错的 SQL,会导致执行失败
  const stmt2 = env.DB.prepare('INSERT INTO non_existent_table (name) VALUES (?)').bind('Bob')

  const stmt3 = env.DB.prepare('INSERT INTO users (name) VALUES (?)').bind('Charlie')

  // 批量执行
  const results = await env.DB.batch([stmt1, stmt2, stmt3])

  console.log('执行成功')
} catch (e) {
  console.error('执行失败,已触发回滚')
  console.error('错误信息:', e.message)

  // 此时,数据库中:
  // 1. Alice 不会被插入(已回滚)
  // 2. Charlie 不会被尝试插入(被跳过)
}

特殊情况与注意事项:

  1. 手动事务控制:虽然 batch() 自带事务,但你也可以在 batch 内部手动写入 BEGINCOMMIT 语句,但这通常是不必要的,且容易导致嵌套事务逻辑混乱。建议直接依赖 batch() 的原子性。

  2. D1 的限制:请注意 D1 的 batch() 有大小限制(例如 SQL 语句的复杂度和参数大小),如果超限可能会在发送请求前就报错。

  3. 非事务性批量执行:如果你希望“能执行多少是多少”(即前一个失败不影响后一个,也不回滚前一个),你不能使用 batch()。你需要自己在代码中用 for 循环配合 await 逐个执行 stmt.run(),并手动用 try-catch 包裹每一个执行操作。但这会导致大量的网络 IO,性能会非常差。

一句话总结: batch() 是全有或全无(All-or-Nothing)的操作,你可以放心地将其作为事务处理机制使用。

D1 执行结果对象的常用属性

调用 D1 执行方法(all/first/get/run)后,会返回执行结果对象,其中包含查询数据执行元数据两大核心属性,是开发中判断执行结果、获取关键信息的依据。

meta

通用核心属性:meta(执行元数据):

所有执行方法的返回结果中均包含 meta 属性,记录 SQL 执行的核心元数据,无查询结果时(如 run()),结果对象仅含 metameta 是一个对象,高频属性如下:

属性名类型核心作用适用场景
changesnumber受 SQL 操作影响的行数(如 UPDATE/DELETE 改了多少行,INSERT 为 1)增/删/改后判断是否执行成功
last_row_idnumber执行 INSERT 后,自增主键(PRIMARY KEY AUTOINCREMENT)的最新 ID新增数据后获取记录 ID
durationnumberSQL 执行耗时(毫秒)性能调试、优化
rows_readnumber执行查询时读取的行数查詢性能优化
rows_writtennumber执行写操作时写入的行数写操作性能优化

示例:获取 meta 中的关键信息

typescript
const res = await c.env.MY_DB.prepare('INSERT INTO users (name, email) VALUES (?, ?)').bind('Bob', 'bob@cf.com').run()
console.log(res.meta.changes) // 1(插入1行)
console.log(res.meta.last_row_id) // 3(假设最新自增ID为3)
console.log(res.meta.duration) // 0.5(执行耗时0.5毫秒)

results

查询类方法专属属性:results:

all()/first() 方法的返回结果包含 results 属性,存储 SQL 查询的数据结果

  • all().results数组,包含所有符合条件的查询结果,每个元素为一条记录的对象(键为列名,值为列值);
  • first().results对象,仅包含第一条查询结果(部分版本中 first() 直接返回结果对象,无需取 results,兼容两种写法)。

示例results 数据结构

typescript
const res = await c.env.MY_DB.prepare('SELECT * FROM users LIMIT 2').all()
console.log(res.results)
// 输出:
// [
//   { id: 1, name: 'Alice', email: 'alice@cf.com', created_at: '2026-01-24T00:00:00Z' },
//   { id: 2, name: 'Bob', email: 'bob@cf.com', created_at: '2026-01-24T00:01:00Z' }
// ]

D1 开发核心注意事项

D1 开发核心注意事项(与方法使用强相关):

  1. 参数绑定是强制规范:严禁直接拼接 SQL 字符串(如 SELECT * FROM users WHERE id = ${id}),必须使用 prepare + bind,防止 SQL 注入;
  2. 执行方法的选型原则
    • 查多条 → all();查单条 → first();查单个值 → get()
    • 增/删/改/建表 → run();批量增/删/改 → batch()
  3. 事务的使用场景:多步 SQL 操作需原子性时使用,避免单步操作开事务(影响性能);
  4. last_insert_rowid():SQL 中可直接使用该函数获取当前连接中最后一次 INSERT 的自增 ID,适合事务中关联多表操作;
  5. 数据类型兼容:D1 基于 SQLite,无严格的类型校验(如整数列可存字符串),建议在代码中做类型校验,保证数据一致性。

R2

什么是 R2

什么是 Cloudflare R2?:

R2 是 Cloudflare 打造的无出口带宽费对象存储服务,兼容亚马逊 S3 核心 API,数据存储在 Cloudflare 全球数据中心集群,同时可通过 Cloudflare 边缘网络实现全球内容分发,无需额外支付数据从存储到用户的出口流量费(这是与 AWS S3、阿里云 OSS 等传统对象存储的核心差异)。

核心定位:

填补 Cloudflare 生态非结构化、大文件、持久化存储的空白:

  • KV 适合小体积、高频读写、键值型数据(单值最大 25MB);
  • D1 适合结构化、关联型SQL 数据;
  • Durable Objects 适合细粒度状态、分布式锁
  • R2 适合非结构化大文件(如图片、视频、静态资源、备份文件、用户上传文件,单文件最大 5TB)。

简单来说:R2 是 Cloudflare 生态的「S3 平替」,无出口费、兼容 S3 API、无缝集成边缘运行时,是边缘应用中存储/分发非结构化文件的首选方案。

R2 核心优势

R2 核心优势(对比传统对象存储/Cloudflare 其他存储):

核心亮点:无出口带宽费:

传统对象存储(S3/OSS)的出口带宽费是核心成本(数据从存储服务器传输到用户的流量费),而 R2 完全免除这一费用——无论多少数据从 R2 分发到全球用户,均不收取出口流量费,仅按存储容量和操作次数计费,大幅降低大文件分发成本。

与传统对象存储的核心对比:

特性维度Cloudflare R2AWS S3/阿里云 OSS(传统对象存储)
出口带宽费完全免费按流量计费(跨区域/公网出口昂贵)
S3 API 兼容兼容核心 S3 API(可直接迁移)原生 S3 API/自研 API
边缘分发能力深度整合 Cloudflare 边缘网络,就近缓存分发需搭配独立 CDN(如 Cloudfront/CDN 加速)
生态集成无缝衔接 Workers/Hono/D1(本地调用,无网络开销)需通过公网/内网 API 调用,有网络延迟
存储成本更低(尤其是大流量分发场景)基础存储成本低,流量成本高

与 Cloudflare 其他存储的适用场景区分:

存储服务存储类型单文件/值上限核心适用场景搭配 R2 用法
R2对象存储5TB图片/视频/静态资源/用户上传文件存储实际文件
KV键值存储25MB/值高频读写的小数据、文件元数据缓存缓存 R2 文件的 URL/大小/哈希等元数据
D1关系型数据库无(按行存储)文件关联的结构化数据(如上传记录)存储文件的用户ID/上传时间/分类等
Durable Objects状态存储无(内存+持久化)大文件上传的断点续传/分布式锁控制 R2 并发上传/文件操作

R2 其他核心优势:

  • S3 无缝迁移:兼容 S3 核心 API(GetObject/PutObject/ListObjects 等),原有基于 S3 的代码可几乎零修改迁移到 R2;
  • 边缘缓存加速:R2 文件可通过 Cloudflare CDN 做边缘缓存,全球用户就近访问,延迟低至毫秒级;
  • 无最小存储期限:无 S3 那样的「最小存储 30 天」限制,临时存储文件也无需支付额外费用;
  • 多存储层级:支持「即时访问」(默认,低延迟)和「低频访问」(更低存储成本,适合冷数据/备份);
  • Workers 本地调用:R2 与 Cloudflare Workers 运行在同一节点,代码中调用 R2 无跨网络请求开销,性能拉满。

R2 前置准备

R2 前置准备:

与 Cloudflare 其他服务一致,R2 的管理和开发依赖 Wrangler,同时需完成 Cloudflare 账户授权,前置步骤如下:

  1. 安装/升级 Wrangler 至 v3.0+(R2 核心功能在 v3 完善):
    bash
    npm install -g wrangler
    # 验证版本
    wrangler -v # 输出 >= 3.0.0 即可
  2. 登录 Cloudflare 账户(授权 Wrangler 访问 R2 资源):
    bash
    wrangler login
  3. (可选)熟悉 S3 基础 API:R2 兼容 S3 核心 API,了解基础用法可降低学习成本(无 S3 基础也可直接学 R2 原生方法)。

R2 常用 Wrangler 命令

R2 基础操作(Wrangler 命令行+配置绑定):

R2 的存储桶(Bucket) 是文件存储的基本单位(类似文件夹,所有文件都在存储桶中),先通过 Wrangler 完成存储桶的创建、管理,并配置到 wrangler.toml 中,才能在代码中访问。

核心概念:R2 存储桶(Bucket):

  • 存储桶是 R2 的顶级命名空间,全球唯一命名(不可与其他 Cloudflare 用户重复);
  • 存储桶包含对象(Object):即实际存储的文件,每个对象有唯一的键(Key)(如 images/avatar.png);
  • 存储桶支持前缀(Prefix):通过键的路径分隔符(/)实现文件分类(如 images/ 前缀下的所有图片)。

模块 1:Wrangler 管理 R2 存储桶

所有存储桶的创建、查询、删除均通过 wrangler r2 命令完成,以下是开发高频用法:

创建 R2 存储桶

创建 R2 存储桶(核心):

bash
# 交互式创建(推荐,自动校验命名唯一性)
wrangler r2 bucket create <BUCKET_NAME> # 如 wrangler r2 bucket create my-first-r2-bucket

# 非交互式创建(指定存储区域,可选:us/europe/apac)
wrangler r2 bucket create <BUCKET_NAME> --location us
  • 执行成功后,终端会提示存储桶创建成功,并可直接用于代码绑定;
  • 命名规则:仅含小写字母、数字、连字符(-),开头/结尾不能为连字符,长度 3-63 位。
列出所有 R2 存储桶

列出所有 R2 存储桶:

bash
wrangler r2 bucket list
  • 输出账户下所有存储桶的名称、创建时间、存储区域,方便核对。
删除 R2 存储桶

删除 R2 存储桶(谨慎):

bash
wrangler r2 bucket delete <BUCKET_NAME>
  • 注意:删除存储桶前需先删除桶内所有文件,否则会执行失败。
查看存储桶内文件

查看存储桶内文件(快速调试):

bash
# 列出存储桶内所有文件
wrangler r2 object list <BUCKET_NAME>

# 按前缀过滤文件(如仅列出 images/ 下的文件)
wrangler r2 object list <BUCKET_NAME> --prefix images/

# 显示文件详细信息(大小、哈希、修改时间)
wrangler r2 object list <BUCKET_NAME> --details

模块 2:将 R2 绑定到 Workers/Hono 项目

需在项目的 wrangler.toml 中配置 R2 绑定,才能在代码中通过 c.env.<BINDING_NAME> 访问存储桶实例,这是代码操作 R2 的前提

配置 wrangler.toml

配置 wrangler.toml(核心):

在项目根目录的 wrangler.toml 中添加 [[r2_buckets]] 节点,配置如下:

toml
# 项目基础配置
name = "r2-hono-demo"
main = "src/index.ts"
compatibility_date = "2026-01-24"

# R2 存储桶绑定(关键!)
[[r2_buckets]]
binding = "MY_R2_BUCKET" # 代码中通过 c.env.MY_R2_BUCKET 访问
bucket_name = "my-first-r2-bucket" # 替换为你的 R2 存储桶名称
  • 一个项目可绑定多个 R2 存储桶,只需添加多个 [[r2_buckets]] 节点即可;
  • 绑定名(binding)建议大写,符合 Cloudflare 生态命名规范。

R2 常用方法

R2 存储桶实例的核心常用方法(代码层操作):

配置绑定后,在 Workers/Hono 代码中通过 c.env.<BINDING_NAME> 访问的R2 存储桶实例(如 c.env.MY_R2_BUCKET)是操作 R2 的唯一入口,所有文件的上传、下载、删除、查询均通过该实例的方法实现。

核心前提:

  1. 所有 R2 方法均为异步方法,需配合 async/await 使用;
  2. R2 文件的键(Key) 是唯一标识(如 avatar.pngimages/2026/01/pic.jpg),支持路径分隔符 \//,建议统一用 / 做分类;
  3. 方法参数支持自定义元数据、缓存控制、内容类型等,适配不同业务场景;
  4. 所有方法均基于 Web Standard API 实现,无 Node.js 依赖,完美适配 Cloudflare 边缘运行时。

R2 存储桶实例的高频方法(按功能分类):

以下方法为 Cloudflare 封装的原生 R2 方法(比 S3 API 更简洁,推荐在 Workers/Hono 中使用),每个方法包含语法、核心作用、Hono 场景示例,可直接落地。

模块 1:文件上传/更新

模块 1:文件上传/更新(put:

向 R2 存储桶中上传新文件覆盖已有文件(相同键会直接覆盖),支持纯文本、二进制、流、FormData 等多种上传方式,是文件操作的核心方法。

  • 语法await bucket.put(key, data, [options])
  • 参数
    • key:字符串,文件唯一键(如 images/avatar.png);
    • data:上传数据,支持 string/ArrayBuffer/Blob/ReadableStream/FormData
    • options:可选对象,配置文件元数据,高频属性:
      • httpMetadata:HTTP 元数据,如 contentType(内容类型,如 image/png)、cacheControl(缓存控制,如 public, max-age=31536000);
      • customMetadata:自定义元数据(如 { author: 'cf', uploadTime: '2026-01-24' });
      • contentLength:文件长度(数字,可选,自动推导)。
  • 返回值:R2 对象实例(包含文件键、大小、哈希、元数据等信息)。
  • Hono 示例(纯文本上传/二进制上传/FormData 表单上传):
typescript
import { Hono } from 'hono'
const app = new Hono()

// 1. 上传纯文本文件
app.post('/r2/upload/text', async (c) => {
  const bucket = c.env.MY_R2_BUCKET
  // 上传纯文本,指定内容类型和缓存控制
  const object = await bucket.put('test.txt', 'Hello Cloudflare R2! 🚀', {
    httpMetadata: {
      contentType: 'text/plain',
      cacheControl: 'public, max-age=86400'
    },
    customMetadata: { author: 'hono-r2' }
  })
  return c.json({
    msg: 'Text file uploaded',
    key: object.key,
    size: object.size,
    etag: object.etag
  })
})

// 2. 上传二进制文件(如前端传的图片/视频)
app.post('/r2/upload/bin', async (c) => {
  const bucket = c.env.MY_R2_BUCKET
  // 读取请求体的二进制数据
  const data = await c.req.blob()
  // 自定义文件键(如 random + 后缀)
  const key = `images/${Date.now()}.png`
  // 上传二进制,自动识别内容类型
  const object = await bucket.put(key, data, {
    httpMetadata: { contentType: data.type }
  })
  return c.json({ msg: 'Binary file uploaded', key: object.key })
})

// 3. 处理 FormData 表单上传(前端常用)
app.post('/r2/upload/form', async (c) => {
  const bucket = c.env.MY_R2_BUCKET
  const form = await c.req.formData()
  // 获取表单中的文件(前端 input name="file")
  const file = form.get('file') as File
  if (!file) return c.json({ msg: 'No file uploaded' }, { status: 400 })
  // 上传文件,使用原文件名作为键
  const object = await bucket.put(file.name, file, {
    httpMetadata: {
      contentType: file.type,
      cacheControl: 'public, max-age=31536000'
    }
  })
  return c.json({ msg: 'Form file uploaded', key: object.key })
})
模块 2:文件下载/读取

模块 2:文件下载/读取(get:

从 R2 存储桶中读取指定键的文件,支持直接返回给客户端(作为响应)或在代码中处理文件内容(如解析、转码),是文件分发的核心方法。

  • 语法await bucket.get(key, [options])
  • 参数
    • key:字符串,文件唯一键;
    • options:可选对象,如 range(字节范围,用于断点续传)、onlyIf(条件下载,如 ifNoneMatch)。
  • 返回值:R2 对象实例(文件存在)/null(文件不存在)。
  • 关键技巧:通过 new Response(object.body, { headers: object.httpMetadata.headers }) 可直接将 R2 文件作为 HTTP 响应返回给客户端,自动携带所有元数据。
  • Hono 示例(文件直接下载/代码中读取文件内容):
typescript
// 1. 直接下载文件(核心用法,前端访问该接口即可下载/预览)
app.get('/r2/download/:key*', async (c) => {
  const bucket = c.env.MY_R2_BUCKET
  // 获取文件键(支持路径,如 /r2/download/images/avatar.png)
  const key = c.req.param('key*')
  const object = await bucket.get(key)
  if (!object) return c.json({ msg: 'File not found' }, { status: 404 })

  // 直接返回 R2 文件作为响应,自动携带内容类型、缓存控制等
  return new Response(object.body, {
    headers: {
      ...object.httpMetadata.headers,
      ETag: object.etag,
      'Content-Length': object.size.toString()
    }
  })
})

// 2. 代码中读取文件内容(如解析文本文件)
app.get('/r2/read/:key', async (c) => {
  const bucket = c.env.MY_R2_BUCKET
  const key = c.req.param('key')
  const object = await bucket.get(key)
  if (!object) return c.json({ msg: 'File not found' }, { status: 404 })

  // 读取文件内容为纯文本
  const content = await object.text()
  // 也可读取为二进制/Blob
  // const buffer = await object.arrayBuffer()
  return c.json({
    key: object.key,
    size: object.size,
    content
  })
})
模块 3:文件删除

模块 3:文件删除(delete:

删除 R2 存储桶中单个/多个文件,支持批量删除,适合文件管理场景。

  • 语法1(单文件删除)await bucket.delete(key)
  • 语法2(批量文件删除)await bucket.delete([key1, key2, key3])
  • 参数:单个键字符串 / 键数组(批量删除);
  • 返回值void(无返回值,删除不存在的文件不会报错)。
  • Hono 示例(单文件删除/批量文件删除):
typescript
// 1. 单文件删除
app.delete('/r2/delete/:key', async (c) => {
  const bucket = c.env.MY_R2_BUCKET
  const key = c.req.param('key')
  await bucket.delete(key)
  return c.json({ msg: 'File deleted successfully' })
})

// 2. 批量文件删除(接收键数组)
app.post('/r2/delete/batch', async (c) => {
  const bucket = c.env.MY_R2_BUCKET
  const { keys } = await c.req.json()
  if (!Array.isArray(keys) || keys.length === 0) {
    return c.json({ msg: 'No keys provided' }, { status: 400 })
  }
  await bucket.delete(keys)
  return c.json({ msg: `Batched delete ${keys.length} files` })
})
模块 4:列出桶内文件

模块 4:列出桶内文件(list:

查询 R2 存储桶中的文件列表,支持前缀过滤、分页、按前缀分组,适合实现「文件管理器」「文件列表查询」等功能。

  • 语法await bucket.list([options])
  • 参数options 为核心配置对象,高频属性:
    • prefix:字符串,按前缀过滤(如 images/,仅列出该前缀下的文件);
    • delimiter:字符串,分隔符(如 /,按路径分组,不显示子目录下的文件);
    • limit:数字,分页大小(最大 1000);
    • cursor:字符串,分页游标(用于下一页查询,从上次返回的 truncatedcursor 获取)。
  • 返回值:对象,包含 objects(文件数组)、prefixes(前缀分组数组)、truncated(是否还有更多文件)、cursor(下一页游标)。
  • Hono 示例(列出所有文件/按前缀过滤/分页查询):
typescript
// 列出文件(支持前缀、分页)
app.get('/r2/list', async (c) => {
  const bucket = c.env.MY_R2_BUCKET
  // 获取查询参数:前缀、分页大小、游标
  const prefix = c.req.query('prefix') || ''
  const limit = Number(c.req.query('limit')) || 10
  const cursor = c.req.query('cursor') || undefined

  const result = await bucket.list({
    prefix,
    limit,
    cursor,
    delimiter: '/' // 按 / 分组,显示目录结构
  })

  // 整理返回数据(仅保留核心信息)
  const files = result.objects.map((obj) => ({
    key: obj.key,
    size: obj.size,
    etag: obj.etag,
    uploadTime: obj.uploaded.toISOString(),
    contentType: obj.httpMetadata.contentType
  }))

  return c.json({
    files,
    prefixes: result.prefixes, // 前缀分组(如 images/)
    truncated: result.truncated, // 是否有更多文件
    nextCursor: result.cursor // 下一页游标
  })
})
模块 5:查询文件元数据

模块 5:查询文件元数据(head:

仅查询文件的元数据(不下载文件内容),比 get 更轻量,适合验证文件是否存在、获取文件大小/类型等场景,减少带宽消耗。

  • 语法await bucket.head(key)
  • 参数:文件唯一键;
  • 返回值:R2 对象实例(仅含元数据,无文件体)/null(文件不存在)。
  • Hono 示例
typescript
app.get('/r2/meta/:key', async (c) => {
  const bucket = c.env.MY_R2_BUCKET
  const key = c.req.param('key')
  const object = await bucket.head(key)
  if (!object) return c.json({ msg: 'File not found' }, { status: 404 })

  return c.json({
    key: object.key,
    size: object.size,
    etag: object.etag,
    uploadTime: object.uploaded.toISOString(),
    contentType: object.httpMetadata.contentType,
    cacheControl: object.httpMetadata.cacheControl,
    customMetadata: object.customMetadata
  })
})

实战:Hono + R2 实现完整的文件管理服务

实战:Hono + R2 实现完整的文件管理服务:

以下是端到端的 Hono + R2 实战示例,实现文件上传、下载、删除、列表查询、元数据查询五大核心功能,可直接复制到项目中使用,只需替换 wrangler.toml 中的 R2 绑定名和存储桶名称。

步骤 1:初始化 Hono + R2 项目:

bash
# 初始化 Workers 项目
wrangler init r2-hono-demo
cd r2-hono-demo
# 安装 Hono
npm install hono
# 创建 R2 存储桶(替换为你的存储桶名称)
wrangler r2 bucket create my-r2-hono-bucket

步骤 2:配置 wrangler.toml:

toml
name = "r2-hono-demo"
main = "src/index.ts"
compatibility_date = "2026-01-24"
node_compat = false # 关闭 Node.js 兼容,纯边缘运行时

# R2 存储桶绑定
[[r2_buckets]]
binding = "MY_R2_BUCKET"
bucket_name = "my-r2-hono-bucket" # 替换为你的存储桶名称

步骤 3:编写核心代码(src/index.ts:

typescript
import { Hono } from 'hono'
import { cors } from 'hono/cors'

const app = new Hono()
// 开启 CORS(解决前端跨域上传/下载)
app.use(
  '*',
  cors({
    origin: '*', // 生产环境指定具体域名
    allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowHeaders: ['Content-Type', 'Authorization']
  })
)

// 全局获取 R2 存储桶实例
const getBucket = (c: any) => c.env.MY_R2_BUCKET

// 1. 表单上传文件(前端常用)
app.post('/upload', async (c) => {
  const bucket = getBucket(c)
  const form = await c.req.formData()
  const file = form.get('file') as File
  if (!file) return c.json({ code: 400, msg: '请选择上传文件' }, { status: 400 })

  // 自定义文件键:前缀 + 时间戳 + 原文件名(避免重复)
  const key = `uploads/${Date.now()}_${file.name.replace(/\s+/g, '_')}`
  try {
    const object = await bucket.put(key, file, {
      httpMetadata: {
        contentType: file.type,
        cacheControl: 'public, max-age=31536000' // 缓存1年
      },
      customMetadata: { uploader: 'hono-r2' }
    })
    return c.json({
      code: 200,
      msg: '上传成功',
      data: {
        key: object.key,
        size: object.size,
        url: `/download/${object.key}` // 下载地址
      }
    })
  } catch (err) {
    return c.json({ code: 500, msg: '上传失败', error: (err as Error).message }, { status: 500 })
  }
})

// 2. 文件下载/预览(支持路径)
app.get('/download/:key*', async (c) => {
  const bucket = getBucket(c)
  const key = c.req.param('key*')
  const object = await bucket.get(key)
  if (!object) return c.json({ code: 404, msg: '文件不存在' }, { status: 404 })

  // 直接返回文件响应
  return new Response(object.body, {
    headers: {
      ...object.httpMetadata.headers,
      ETag: object.etag,
      'Content-Length': object.size.toString(),
      'Content-Disposition': `inline; filename="${encodeURIComponent(object.key.split('/').pop()!)}"`
    }
  })
})

// 3. 删除文件
app.delete('/delete/:key', async (c) => {
  const bucket = getBucket(c)
  const key = c.req.param('key')
  try {
    await bucket.delete(key)
    return c.json({ code: 200, msg: '删除成功' })
  } catch (err) {
    return c.json({ code: 500, msg: '删除失败', error: (err as Error).message }, { status: 500 })
  }
})

// 4. 列出文件(支持前缀、分页)
app.get('/list', async (c) => {
  const bucket = getBucket(c)
  const prefix = c.req.query('prefix') || 'uploads/'
  const limit = Number(c.req.query('limit')) || 20
  const cursor = c.req.query('cursor') || undefined

  const result = await bucket.list({ prefix, limit, cursor, delimiter: '/' })
  const files = result.objects.map((obj) => ({
    key: obj.key,
    size: obj.size,
    uploadTime: obj.uploaded.toISOString(),
    contentType: obj.httpMetadata.contentType,
    url: `/download/${obj.key}`
  }))

  return c.json({
    code: 200,
    data: {
      files,
      prefixes: result.prefixes,
      hasMore: result.truncated,
      nextCursor: result.cursor
    }
  })
})

// 5. 查询文件元数据
app.get('/meta/:key', async (c) => {
  const bucket = getBucket(c)
  const key = c.req.param('key')
  const object = await bucket.head(key)
  if (!object) return c.json({ code: 404, msg: '文件不存在' }, { status: 404 })

  return c.json({
    code: 200,
    data: {
      key: object.key,
      size: object.size,
      etag: object.etag,
      uploadTime: object.uploaded.toISOString(),
      contentType: object.httpMetadata.contentType,
      cacheControl: object.httpMetadata.cacheControl,
      customMetadata: object.customMetadata
    }
  })
})

// 导出 Workers 处理函数
export default app.fetch

步骤 4:本地调试与线上部署:

bash
# 本地调试(启动开发服务器,默认 8787 端口)
wrangler dev

# 部署到 Cloudflare 线上
wrangler deploy

步骤 5:前端简单测试(HTML 表单上传):

创建 index.html,直接打开即可测试上传功能:

html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>R2 + Hono 文件上传</title>
  </head>
  <body>
    <form id="uploadForm" enctype="multipart/form-data">
      <input type="file" name="file" accept="*" />
      <button type="submit">上传文件</button>
    </form>
    <script>
      const form = document.getElementById('uploadForm')
      form.onsubmit = async (e) => {
        e.preventDefault()
        const formData = new FormData(form)
        const res = await fetch('http://localhost:8787/upload', {
          method: 'POST',
          body: formData
        })
        const data = await res.json()
        console.log('上传结果:', data)
        if (data.code === 200) {
          alert(`上传成功,下载地址:${data.data.url}`)
        }
      }
    </script>
  </body>
</html>

R2 进阶技巧

结合 Cloudflare 自定义域名分发 R2 文件

结合 Cloudflare 自定义域名分发 R2 文件:

将 R2 绑定到 Cloudflare 自定义域名(如 cdn.xxx.com),通过 https://cdn.xxx.com/avatar.png 直接访问文件,替代 Workers 路由,提升分发性能:

  • 操作步骤:Cloudflare 仪表盘 → 你的域名 → Workers 路由 → 添加路由 cdn.xxx.com/* → 绑定到你的 R2 存储桶。

设置 R2 存储桶 CORS 规则

设置 R2 存储桶 CORS 规则:

解决前端直接访问 R2 文件的跨域问题(无需 Workers 开启 CORS):

  • Cloudflare 仪表盘 → R2 → 你的存储桶 → 设置 → CORS 规则 → 添加规则,示例:
    json
    [
      {
        "AllowedOrigins": ["*"],
        "AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
        "AllowedHeaders": ["*"],
        "ExposeHeaders": ["ETag", "Content-Length"]
      }
    ]

配置 R2 生命周期规则

配置 R2 生命周期规则(自动清理文件):

对临时文件/日志文件,设置生命周期规则自动删除,避免手动维护:

  • Cloudflare 仪表盘 → R2 → 你的存储桶 → 设置 → 生命周期规则 → 添加规则:
    • 示例1:删除 30 天未访问的文件;
    • 示例2:删除前缀为 temp/ 的所有文件;
    • 示例3:将 90 天未访问的文件转为「低频访问」存储(降低成本)。

断点续传

断点续传(大文件上传):

利用 R2 的 put 方法支持ReadableStreamget 方法的 range 参数,结合 Durable Objects 实现大文件断点续传,适合上传 GB 级别的视频/压缩包。

文件元数据缓存

文件元数据缓存(结合 KV):

将 R2 文件的元数据(键、大小、URL、类型)缓存到 Cloudflare KV 中,替代频繁调用 list/head 方法,提升查询性能:

  • 上传文件时:同时将元数据写入 KV;
  • 查询文件列表时:先从 KV 读取,再按需从 R2 同步。

R2 开发注意事项

存储桶与文件键命名规则

存储桶与文件键命名规则:

  • 存储桶:全球唯一,仅含小写字母、数字、连字符,3-63 位;
  • 文件键:支持 UTF-8 字符,建议避免空格/特殊字符(可替换为 _),通过前缀(如 images//uploads/)做分类。

文件大小限制

文件大小限制:

  • 单文件最大上传:5TB;
  • Workers 运行时单次请求内存限制:128MB/256MB,上传/下载超大文件时需使用流(Stream),避免加载整个文件到内存。

权限控制

权限控制:

  • 公共访问:默认 R2 存储桶为私有,需通过 Workers/Hono 做权限控制(如添加 Token 认证、用户鉴权);
  • 私有文件:在 Workers 代码中添加鉴权逻辑(如验证 Authorization 请求头),仅授权用户可访问/下载。

计费规则

计费规则(免费版/付费版):

  • 免费版配额:10GB 存储、100 万次读操作、10 万次写操作、10GB 数据处理,满足小型应用开发;
  • 付费版:按实际使用量计费,存储费≈$0.015/GB/月,操作费远低于 S3,无出口带宽费(核心优势)。

S3 API 兼容使用

S3 API 兼容使用:

若需使用 S3 原生 API 操作 R2(如通过 AWS CLI、S3 客户端),可在 Cloudflare 仪表盘获取 R2 的S3 兼容端点、Access Key、Secret Key,配置后即可无缝使用。

R2 最佳实践与适用场景

核心适用场景

核心适用场景:

  • 静态资源存储与分发:图片、视频、CSS/JS、字体文件等,结合 Cloudflare 边缘缓存实现全球低延迟分发;
  • 用户上传文件存储:前端表单上传的头像、附件、文档,通过 Hono/Workers 做上传校验/权限控制;
  • 大文件备份/存储:备份文件、日志文件、数据库快照,无出口费,适合跨云分发;
  • 边缘数据处理:通过 Workers 读取 R2 中的文件,在边缘做实时处理(如图片压缩、文本解析、视频转码),处理后再写回 R2。

Cloudflare 生态整合最佳实践

Cloudflare 生态整合最佳实践:

  • R2 + Hono/Workers:实现文件的上传/下载/管理/鉴权,打造边缘文件服务;
  • R2 + KV:KV 缓存文件元数据,R2 存储实际文件,提升查询性能;
  • R2 + D1:D1 存储文件的结构化关联数据(如用户上传记录、文件分类、访问统计),R2 存储文件本体;
  • R2 + Durable Objects:实现大文件断点续传、分布式锁、并发上传控制;
  • R2 + Pages:为 Cloudflare Pages 静态站点提供文件上传/存储能力(如博客的图片上传、静态站的附件管理)。

KV

什么是 KV

什么是 Cloudflare KV:

Cloudflare KV(Key-Value)是无服务器、全球分布式的边缘键值存储,数据会同步部署到 Cloudflare 全球 300+ 边缘数据中心,用户请求会被路由到最近的边缘节点获取数据,延迟低至毫秒级

KV 采用简单的键值对模型,无复杂的查询语法,API 极简,同时支持键过期(TTL)、自定义元数据、批量操作,核心定位是解决边缘应用中小体积、高频访问、无需关联查询的数据存储需求。

核心定位:Cloudflare 生态的「基础存储层」:

KV 是 Cloudflare 最早推出的存储服务,填补了边缘高频小数据存储的空白,与其他 Cloudflare 存储服务形成明确的场景互补(这是使用 KV 的核心前提,避免用错场景):

存储服务核心类型数据体积限制核心优势核心适用场景
KV键值存储单值最大 25MB,键≤512B全球边缘、毫秒级读写、高可用配置存储、接口缓存、会话信息、高频小数据、元数据缓存
D1关系型数据库无(按行存储)SQL 关联查询、事务结构化/关联型数据(用户信息、订单、文件记录)
R2对象存储单文件最大 5TB无出口费、S3 兼容、大文件非结构化大文件(图片、视频、用户上传文件、静态资源)
Durable Objects状态存储内存+持久化,无硬限细粒度状态、分布式锁、强一致实时状态、断点续传、并发控制、强一致性数据

简单来说:KV 是 Cloudflare 边缘开发的「轻量缓存/持久化二合一方案」,既可以做临时缓存(设置 TTL 自动过期),也可以做持久化存储(无 TTL 永久保存),是高频小数据场景的最优解。

KV 核心优势

边缘原生,极致低延迟:

数据同步到 Cloudflare 全球边缘节点,用户请求就近访问,无需跨区域调用中心化数据库,平均延迟比传统云数据库低一个数量级,适合对响应速度要求高的场景(如接口缓存、实时配置)。

无服务器,免运维:

无需部署、管理服务器,无需关注扩容/缩容,Cloudflare 自动负责数据的分布式同步、容灾、高可用,开发者只需通过 API/Wrangler 操作键值对,零运维成本

API 极简,生态无缝集成:

  • 与 Cloudflare Workers/Hono/Pages 深度整合,代码中通过 c.env.<BINDING_NAME> 直接访问,无网络请求开销(与运行时同节点);
  • 提供 HTTP API、Wrangler 命令行、仪表盘等多种操作方式,适配开发/运维不同场景;
  • 所有方法基于 Web Standard API 实现,无 Node.js 依赖,完美适配边缘运行时。

支持过期时间(TTL)与自定义元数据:

  • TTL(Time-To-Live):可为键值对设置过期时间,自动删除过期数据,适合缓存场景(如接口数据缓存 5 分钟),无需手动清理;
  • 自定义元数据:可为每个键绑定额外的元数据(如 JSON 对象),存储键的附加信息(如数据更新时间、来源),无需单独存储。

高可用与高并发:

  • 多节点容灾,单节点故障不影响服务,可用性达 99.99%;
  • 支持超高并发读写,适合流量高峰场景(如电商秒杀的配置缓存、热点数据访问)。

免费额度友好:

开发测试阶段完全免费,免费版配额:1GB 存储、1000 万次读操作/月、100 万次写操作/月,满足小型边缘应用的全部需求。

KV 核心概念

使用 KV 前需理解 3 个核心概念,这是避免操作错误、合理设计键值结构的基础:

命名空间

命名空间(Namespace):

命名空间(Namespace)是 KV 键值对的「顶级容器」,所有键值对都必须属于某个命名空间,相当于 KV 的「数据库」。

  • 全球唯一:每个命名空间有唯一的 ID名称,账户内命名空间名称不可重复;
  • 独立隔离:不同命名空间的键值对完全隔离,互不影响(如「dev_config」开发环境、「prod_config」生产环境,避免数据混淆);
  • 多绑定支持:一个 Cloudflare Workers/Pages 项目可绑定多个命名空间,实现数据分类管理(如一个命名空间存缓存,一个存系统配置)。

键(Key):

键是 KV 中数据的唯一标识,用于读取、更新、删除对应的值,设计键时需遵循规则和最佳实践:

  • 命名规则:最大长度 512 字节,支持字母、数字、特殊字符(-/_:.~),区分大小写(如 User1user1 是两个不同的键);
  • 最佳实践:采用分层命名(如 config/app/titlecache/api/user/123),通过分隔符 / 实现键的分类,方便管理和批量查询;
  • 唯一性:同一命名空间内键不可重复,重复写入会直接覆盖原有值。

值(Value):

值是 KV 中存储的实际数据,需注意大小限制数据类型

  • 大小限制单值最大 25MB,这是 KV 最核心的限制,超过该大小会写入失败(大文件请用 R2);
  • 数据类型:原生仅支持二进制数据/字符串,如需存储 JSON/对象,需在代码中手动序列化(JSON.stringify)反序列化(JSON.parse)
  • 编码:建议使用 UTF-8 编码存储字符串,避免乱码。

附加属性

附加属性(TTL + 元数据):

每个键值对可绑定两个附加属性,无需单独存储,提升开发效率:

  • TTL:过期时间,单位为,设置后键值对会在指定时间后自动删除;也可设置为绝对时间戳(Unix 时间戳,秒级),适合精准过期;
  • 自定义元数据:键值对的附加信息,为键值对对象(最大 16KB),如 { "updateTime": 1735689600, "source": "api" },用于存储键的附加信息,不影响值的内容。

KV 前置准备

KV 前置准备:

与 Cloudflare 其他服务一致,KV 的管理和开发依赖 Wrangler v3.0+,同时需完成 Cloudflare 账户授权,前置步骤如下:

  1. 安装/升级 Wrangler 至 v3.0+:
    bash
    npm install -g wrangler
    # 验证版本
    wrangler -v # 输出 >= 3.0.0 即可
  2. 登录 Cloudflare 账户(授权 Wrangler 访问 KV 资源):
    bash
    wrangler login

KV 常用 Wrangler 命令

KV 的命名空间管理(创建/删除/列表)主要通过 Wrangler 命令行完成,而键值对操作可通过命令行、代码、Cloudflare 仪表盘实现。核心前提:需将 KV 命名空间绑定到 Workers/Hono 项目,才能在代码中访问。

模块 1:Wrangler 管理 KV 命名空间

所有命名空间操作均通过 wrangler kv:namespace 命令完成,以下是开发中最常用的命令:

创建 KV 命名空间

创建 KV 命名空间(核心):

bash
# 交互式创建(推荐,自动生成命名空间)
wrangler kv:namespace create <NAMESPACE_NAME> # 如 wrangler kv:namespace create my-first-kv

# 为不同环境创建命名空间(如开发/生产,推荐)
wrangler kv:namespace create <NAMESPACE_NAME> --env dev # 开发环境
wrangler kv:namespace create <NAMESPACE_NAME> --env prod # 生产环境
  • 执行成功后,终端会输出命名空间 ID绑定配置代码必须复制到 wrangler.toml,这是代码访问 KV 的关键);
  • 示例输出:
    ✨ Successfully created KV namespace "my-first-kv" (ID: 12345678-1234-1234-1234-1234567890ab)
    ⚡️ Add the following to your wrangler.toml:
    [[kv_namespaces]]
    binding = "MY_KV" # 代码中通过 c.env.MY_KV 访问
    id = "12345678-1234-1234-1234-1234567890ab" # 命名空间唯一 ID
    # preview_id = "xxx" # 预览环境 ID,自动生成
列出所有 KV 命名空间

列出所有 KV 命名空间:

bash
wrangler kv:namespace list
  • 输出账户下所有命名空间的名称、ID、环境,方便核对命名空间是否创建成功。
删除 KV 命名空间

删除 KV 命名空间(谨慎):

bash
wrangler kv:namespace delete <NAMESPACE_NAME> --id <NAMESPACE_ID>
  • 注意:删除命名空间会永久删除其中所有键值对,且无法恢复,需谨慎操作。
重命名 KV 命名空间

重命名 KV 命名空间:

bash
wrangler kv:namespace rename <OLD_NAME> <NEW_NAME> --id <NAMESPACE_ID>

模块 2:将 KV 命名空间绑定到 Workers/Hono 项目

需在项目的 wrangler.toml 中添加 [[kv_namespaces]] 节点,将命名空间与项目绑定,这是代码中访问 KV 的唯一方式

配置 wrangler.toml

配置 wrangler.toml(核心):

将创建命名空间时输出的配置代码复制到 wrangler.toml,示例如下:

toml
# 项目基础配置
name = "kv-hono-demo"
main = "src/index.ts"
compatibility_date = "2026-01-24"

# KV 命名空间绑定(关键!)
[[kv_namespaces]]
binding = "MY_KV" # 代码中通过 c.env.MY_KV 访问该命名空间
id = "12345678-1234-1234-1234-1234567890ab" # 替换为你的命名空间 ID
# preview_id = "98765432-4321-4321-4321-ba0987654321" # 预览环境 ID,自动生成

# 多环境绑定(开发/生产,推荐)
[env.dev]
[[env.dev.kv_namespaces]]
binding = "MY_KV"
id = "dev-namespace-id-123"

[env.prod]
[[env.prod.kv_namespaces]]
binding = "MY_KV"
id = "prod-namespace-id-456"

# 绑定多个命名空间(数据分类)
[[kv_namespaces]]
binding = "CACHE_KV" # 缓存专用
id = "cache-namespace-id-789"
[[kv_namespaces]]
binding = "CONFIG_KV" # 配置专用
id = "config-namespace-id-000"
  • 绑定名(binding)建议大写,符合 Cloudflare 生态命名规范;
  • 一个项目可绑定多个命名空间,实现数据的隔离管理(如缓存和配置分开);
  • 多环境绑定可避免开发环境数据污染生产环境。

模块 3:Wrangler 操作 KV 键值对

模块 3:Wrangler 操作 KV 键值对(快速调试):

开发中可通过 Wrangler 命令行快速操作键值对,适合调试、初始化测试数据,无需编写代码:

bash
# 写入/更新键值对(--ttl 可选,设置过期时间,单位秒)
wrangler kv:key put --namespace-id <NAMESPACE_ID> "config/app/title" "Cloudflare KV + Hono Demo" --ttl 3600

# 读取键值对
wrangler kv:key get --namespace-id <NAMESPACE_ID> "config/app/title"

# 删除键值对
wrangler kv:key delete --namespace-id <NAMESPACE_ID> "config/app/title"

# 列出命名空间内的所有键(--prefix 可选,按前缀过滤)
wrangler kv:key list --namespace-id <NAMESPACE_ID> --prefix "config/"

KV 常用实例方法

KV 命名空间实例的核心常用方法(代码层操作):

配置绑定后,在 Workers/Hono 代码中通过 c.env.<BINDING_NAME> 访问的KV 命名空间实例(如 c.env.MY_KV)是操作 KV 的唯一入口,所有键值对的增、删、改、查、批量操作均通过该实例的方法实现。

核心前提:

  1. 所有 KV 方法均为异步方法,需配合 async/await 使用;
  2. KV 仅支持字符串/二进制值,存储 JSON/对象时需手动 JSON.stringify 序列化,读取时 JSON.parse 反序列化;
  3. 方法参数支持TTL 过期时间自定义元数据,适配缓存、配置等不同场景;
  4. 所有方法基于 Web Standard API 实现,无 Node.js 依赖,完美适配 Cloudflare 边缘运行时。

KV 实例的高频方法(按功能分类,附 Hono 示例):

以下方法为 Cloudflare 封装的原生 KV 方法,是开发中 99% 场景会用到的核心方法,每个方法包含语法、核心作用、参数说明、Hono 实战示例,可直接落地使用。

模块 1:写入/更新键值对

模块 1:写入/更新键值对(put:

向 KV 命名空间中写入新键值对覆盖已有键值对(相同键直接覆盖),支持设置 TTL 过期时间和自定义元数据,是 KV 写入操作的核心方法。

  • 语法await kvInstance.put(key, value, [options])
  • 参数
    • key:字符串,键名(遵循 512B 限制,建议分层命名);
    • value:字符串/ArrayBuffer/Uint8Array,值(遵循 25MB 限制);
    • options:可选配置对象,高频属性:
      • ttl:数字,过期时间(单位:秒),设置后键值对自动过期删除;
      • expiration:数字,绝对过期时间(Unix 时间戳,秒级),优先级高于 ttl
      • metadata:对象,自定义元数据(最大 16KB),如 { updateTime: Date.now(), source: "hono" }
  • 返回值Promise<void>,无返回值,写入成功即resolve。
  • Hono 示例(存储字符串/JSON 对象/设置 TTL/元数据):
typescript
import { Hono } from 'hono'
const app = new Hono()

// 全局获取 KV 实例
const getKV = (c: any) => c.env.MY_KV

// 1. 存储简单字符串(如系统配置)
app.post('/kv/put/string', async (c) => {
  const kv = getKV(c)
  const { key, value } = await c.req.json()
  if (!key || !value) return c.json({ code: 400, msg: 'key 和 value 为必传' }, { status: 400 })

  await kv.put(key, value)
  return c.json({ code: 200, msg: '写入成功', data: { key, value } })
})

// 2. 存储 JSON 对象(需序列化,核心用法)
app.post('/kv/put/json', async (c) => {
  const kv = getKV(c)
  const { key, data } = await c.req.json() // data 为任意 JSON 对象
  if (!key || !data) return c.json({ code: 400, msg: 'key 和 data 为必传' }, { status: 400 })

  // 序列化 JSON 对象为字符串
  const value = JSON.stringify(data)
  // 设置元数据 + 5 分钟 TTL 过期
  await kv.put(key, value, {
    ttl: 300, // 5分钟后自动过期
    metadata: {
      updateTime: Date.now(),
      type: 'json',
      source: 'hono-api'
    }
  })
  return c.json({ code: 200, msg: 'JSON 对象写入成功', data: { key } })
})

// 3. 存储二进制数据(如小图片/小文件,<25MB)
app.post('/kv/put/bin', async (c) => {
  const kv = getKV(c)
  const key = 'bin/avatar-small.png'
  // 读取二进制请求体
  const buffer = await c.req.arrayBuffer()
  // 写入二进制数据
  await kv.put(key, buffer, { metadata: { type: 'image/png' } })
  return c.json({ code: 200, msg: '二进制数据写入成功', data: { key } })
})
模块 2:读取键值对

模块 2:读取键值对(get:

从 KV 命名空间中读取指定键的对应值,支持仅读取、同时读取值+元数据,是 KV 读取操作的核心方法。

  • 语法1(仅读取值)await kvInstance.get(key, [type])
  • 语法2(读取值+元数据)await kvInstance.getWithMetadata(key, [type])
  • 参数
    • key:字符串,键名;
    • type:可选,值的类型,用于自动解析,支持:
      • 'text'(默认):返回字符串;
      • 'json':自动反序列化为 JSON 对象(无需手动 JSON.parse);
      • 'arrayBuffer':返回 ArrayBuffer 二进制数据;
      • 'stream':返回 ReadableStream 流。
  • 返回值
    • get:返回对应类型的值,键不存在则返回 null
    • getWithMetadata:返回对象 { value: 对应类型的值, metadata: 自定义元数据对象 },键不存在则返回 null。
  • Hono 示例(读取字符串/自动解析 JSON/读取二进制/读取值+元数据):
typescript
// 1. 读取简单字符串(默认 text 类型)
app.get('/kv/get/string/:key', async (c) => {
  const kv = getKV(c)
  const key = c.req.param('key')
  const value = await kv.get(key)

  if (value === null) return c.json({ code: 404, msg: '键不存在' }, { status: 404 })
  return c.json({ code: 200, msg: '读取成功', data: { key, value } })
})

// 2. 读取 JSON 对象(指定 type: 'json',自动反序列化,核心用法)
app.get('/kv/get/json/:key', async (c) => {
  const kv = getKV(c)
  const key = c.req.param('key')
  // 自动解析为 JSON 对象,无需手动 JSON.parse
  const data = await kv.get(key, { type: 'json' })

  if (data === null) return c.json({ code: 404, msg: '键不存在' }, { status: 404 })
  return c.json({ code: 200, msg: '读取成功', data: { key, data } })
})

// 3. 读取二进制数据(如小图片)
app.get('/kv/get/bin/:key', async (c) => {
  const kv = getKV(c)
  const key = c.req.param('key')
  const buffer = await kv.get(key, { type: 'arrayBuffer' })

  if (buffer === null) return c.json({ code: 404, msg: '键不存在' }, { status: 404 })
  // 直接返回二进制响应(如图片预览)
  return new Response(buffer, {
    headers: { 'Content-Type': 'image/png' }
  })
})

// 4. 读取值 + 元数据(核心,适合获取键的附加信息)
app.get('/kv/get/meta/:key', async (c) => {
  const kv = getKV(c)
  const key = c.req.param('key')
  const result = await kv.getWithMetadata(key, { type: 'json' })

  if (result === null) return c.json({ code: 404, msg: '键不存在' }, { status: 404 })
  return c.json({
    code: 200,
    msg: '读取成功',
    data: {
      key,
      value: result.value,
      metadata: result.metadata || {} // 元数据
    }
  })
})
模块 3:删除键值对

模块 3:删除键值对(delete:

删除 KV 命名空间中指定的键值对,删除不存在的键不会报错,无返回值,是 KV 删除操作的核心方法。

  • 语法await kvInstance.delete(key)
  • 参数key 为字符串,键名;
  • 返回值Promise<void>,无返回值,删除成功即resolve;
  • Hono 示例
typescript
app.delete('/kv/delete/:key', async (c) => {
  const kv = getKV(c)
  const key = c.req.param('key')

  await kv.delete(key) // 删除不存在的键不会报错
  return c.json({ code: 200, msg: '删除成功', data: { key } })
})
模块 4:列出命名空间内的键

模块 4:列出命名空间内的键(list:

查询 KV 命名空间中的键列表,支持按前缀过滤分页,适合实现键的批量管理、分层查询(如列出所有 config/ 前缀的配置键)。

  • 语法await kvInstance.list([options])
  • 参数options 为可选配置对象,高频属性:
    • prefix:字符串,按前缀过滤键(如 'config/app/',仅列出该前缀下的键);
    • limit:数字,分页大小(最大 1000,默认 1000);
    • cursor:字符串,分页游标(用于下一页查询,从上次返回的 cursor 获取)。
  • 返回值:对象,包含:
    • keys:键数组,每个元素为 { name: 键名, expiration?: 过期时间戳, metadata?: 元数据 }
    • cursor:下一页游标,若 cursornull,表示无更多键;
    • list_complete:布尔值,是否查询完成(true 表示无更多键)。
  • Hono 示例(列出所有键/按前缀过滤/分页查询):
typescript
app.get('/kv/list', async (c) => {
  const kv = getKV(c)
  // 获取查询参数:前缀、分页大小、游标
  const prefix = c.req.query('prefix') || ''
  const limit = Number(c.req.query('limit')) || 100
  const cursor = c.req.query('cursor') || undefined

  const result = await kv.list({ prefix, limit, cursor })
  return c.json({
    code: 200,
    msg: '查询成功',
    data: {
      keys: result.keys,
      cursor: result.cursor, // 下一页游标
      hasMore: !result.list_complete // 是否有更多键
    }
  })
})
模块 5:批量操作

模块 5:批量操作(putMany / deleteMany:

KV 支持批量写入批量删除键值对,比循环调用 put/delete 更高效(减少边缘节点与 KV 存储的交互次数),适合初始化数据、批量清理缓存等场景。

批量写入

批量写入(putMany:

  • 语法await kvInstance.putMany(entries, [options])
  • 参数
    • entries:数组,每个元素为单个键值对的配置,格式:{ key: 键名, value: 值, ttl?: 过期时间, expiration?: 绝对过期时间, metadata?: 元数据 }
    • options:可选,全局配置(如统一设置 TTL)。
  • 返回值Promise<void>,无返回值。
批量删除

批量删除(deleteMany:

  • 语法await kvInstance.deleteMany(keys)
  • 参数keys 为字符串数组,包含需要删除的键名;
  • 返回值Promise<void>,无返回值。
示例:Hono 批量操作
typescript
// 批量写入键值对(初始化系统配置)
app.post('/kv/put/batch', async (c) => {
  const kv = getKV(c)
  // 批量写入的键值对数组
  const entries = [
    { key: 'config/app/title', value: 'KV + Hono Demo', ttl: 86400 },
    {
      key: 'config/app/version',
      value: '1.0.0',
      metadata: { updateTime: Date.now() }
    },
    { key: 'config/app/enable', value: 'true', ttl: 3600 }
  ]
  // 也可接收前端传的批量数据
  // const { entries } = await c.req.json()

  await kv.putMany(entries)
  return c.json({
    code: 200,
    msg: '批量写入成功',
    data: { count: entries.length }
  })
})

// 批量删除键值对(清理指定前缀的缓存)
app.post('/kv/delete/batch', async (c) => {
  const kv = getKV(c)
  const { keys } = await c.req.json() // 前端传的键数组:["cache/api/1", "cache/api/2"]
  if (!Array.isArray(keys) || keys.length === 0) {
    return c.json({ code: 400, msg: 'keys 为必传的数组' }, { status: 400 })
  }

  await kv.deleteMany(keys)
  return c.json({
    code: 200,
    msg: '批量删除成功',
    data: { count: keys.length }
  })
})

实战:Hono + KV 实现高频接口缓存服务

实战:Hono + KV 实现高频接口缓存服务:

KV 最经典的应用场景是接口数据缓存,通过缓存高频访问的接口数据,避免重复调用底层服务(如 D1/R2/第三方 API),提升接口响应速度,降低底层服务压力。以下是生产级别的 Hono + KV 缓存服务实战,包含缓存自动过期、缓存穿透防护、缓存键自动生成,可直接复用。

实战需求:

实现一个用户信息接口的缓存层:

  1. 首次访问 /api/user/:id 时,从 D1 读取用户数据,同时将数据缓存到 KV(设置 5 分钟过期);
  2. 5 分钟内再次访问该接口,直接从 KV 读取缓存数据,无需访问 D1;
  3. 缓存过期后,自动重新从 D1 读取并更新缓存;
  4. 防护缓存穿透(键不存在时,缓存空值并设置短时间过期)。

实战步骤:

步骤 1:配置 wrangler.toml:

已绑定 KV 命名空间(MY_KV)和 D1 数据库(MY_DB),参考之前的 D1/KV 配置。

步骤 2:编写核心代码(src/index.ts:

typescript
import { Hono } from 'hono'
import { cors } from 'hono/cors'

const app = new Hono()
app.use('*', cors()) // 解决跨域

// 全局获取 KV 和 D1 实例
const getKV = (c: any) => c.env.MY_KV
const getDB = (c: any) => c.env.MY_DB

// 缓存键生成工具(避免硬编码,统一前缀)
const generateCacheKey = (prefix: string, id: string | number) => `cache/${prefix}/${id}`
// 缓存过期时间:5 分钟(300 秒)
const CACHE_TTL = 300
// 缓存穿透空值过期时间:10 秒
const EMPTY_CACHE_TTL = 10

// 从 D1 读取用户数据(底层服务)
const getUserFromDB = async (db: any, id: number) => {
  const { results } = await db.prepare('SELECT * FROM users WHERE id = ?').bind(id).all()
  return results.length > 0 ? results[0] : null
}

// 核心:带缓存的用户信息接口
app.get('/api/user/:id', async (c) => {
  const kv = getKV(c)
  const db = getDB(c)
  const userId = Number(c.req.param('id'))

  if (isNaN(userId)) return c.json({ code: 400, msg: '用户ID为数字' }, { status: 400 })

  // 1. 生成缓存键
  const cacheKey = generateCacheKey('api/user', userId)

  // 2. 从 KV 读取缓存
  const cacheData = await kv.get(cacheKey, { type: 'json' })

  // 3. 缓存命中:直接返回缓存数据
  if (cacheData !== null) {
    // 区分真实数据和空值(防护缓存穿透)
    if (cacheData === 'EMPTY') return c.json({ code: 404, msg: '用户不存在' }, { status: 404 })
    return c.json({
      code: 200,
      msg: '缓存命中',
      data: cacheData,
      from: 'kv-cache'
    })
  }

  // 4. 缓存未命中:从 D1 读取数据
  const userData = await getUserFromDB(db, userId)

  // 5. 处理数据:存在则缓存,不存在则缓存空值(防护穿透)
  if (userData) {
    // 缓存真实数据,设置 5 分钟过期
    await kv.put(cacheKey, JSON.stringify(userData), { ttl: CACHE_TTL })
    return c.json({
      code: 200,
      msg: '缓存未命中,从数据库读取',
      data: userData,
      from: 'd1-db'
    })
  } else {
    // 缓存空值(标记为 EMPTY),设置 10 秒过期,避免重复查询不存在的用户
    await kv.put(cacheKey, 'EMPTY', { ttl: EMPTY_CACHE_TTL })
    return c.json({ code: 404, msg: '用户不存在' }, { status: 404 })
  }
})

// 手动刷新指定用户的缓存
app.post('/api/cache/refresh/user/:id', async (c) => {
  const kv = getKV(c)
  const db = getDB(c)
  const userId = Number(c.req.param('id'))

  if (isNaN(userId)) return c.json({ code: 400, msg: '用户ID为数字' }, { status: 400 })

  const cacheKey = generateCacheKey('api/user', userId)
  const userData = await getUserFromDB(db, userId)

  if (userData) {
    await kv.put(cacheKey, JSON.stringify(userData), { ttl: CACHE_TTL })
    return c.json({ code: 200, msg: '缓存刷新成功', data: userData })
  } else {
    await kv.delete(cacheKey)
    return c.json({ code: 404, msg: '用户不存在,已删除缓存' }, { status: 404 })
  }
})

// 批量清理用户缓存
app.delete('/api/cache/clear/user', async (c) => {
  const kv = getKV(c)
  // 列出所有用户缓存键
  const { keys } = await kv.list({ prefix: 'cache/api/user/' })
  const cacheKeys = keys.map((key) => key.name)

  if (cacheKeys.length > 0) await kv.deleteMany(cacheKeys)
  return c.json({
    code: 200,
    msg: '用户缓存清理成功',
    data: { count: cacheKeys.length }
  })
})

export default app.fetch

步骤 3:本地调试与部署:

bash
# 本地调试
wrangler dev
# 线上部署
wrangler deploy

实战效果:

  1. 首次访问 http://localhost:8787/api/user/1 → 缓存未命中,从 D1 读取并缓存;
  2. 5 分钟内再次访问 → 缓存命中,直接从 KV 返回,响应速度提升至毫秒级;
  3. 访问不存在的用户 http://localhost:8787/api/user/999 → 缓存空值 10 秒,10 秒内重复访问不会查询 D1;
  4. 调用刷新接口 → 手动更新缓存,适用于用户数据修改后同步缓存。

KV 进阶技巧

KV 进阶技巧

合理设计键的分层结构:

采用 前缀/模块/标识 的分层命名规则,如 cache/api/user/123config/app/titlesession/user/456,优势:

  • 方便通过 list 方法按前缀过滤,实现批量管理;
  • 键的含义清晰,易于维护;
  • 便于后续按模块拆分到不同的命名空间。

利用 TTL 实现多级缓存:

为不同类型的数据设置不同的 TTL,实现多级缓存

  • 热点数据:TTL 设为 5-10 分钟,高频刷新;
  • 静态配置:TTL 设为 1-24 小时,减少更新频率;
  • 临时数据:TTL 设为几秒到几分钟,自动清理。

防护缓存穿透/缓存雪崩:

  • 缓存穿透:查询不存在的键时,缓存空值并设置短时间 TTL(如 10 秒),避免重复查询底层服务;
  • 缓存雪崩:为不同的键设置随机 TTL(如 300±10 秒),避免大量键同时过期,导致底层服务压力骤增。

结合 Cloudflare 缓存规则:

将 KV 缓存的接口与 Cloudflare 全局缓存规则结合,实现边缘双层缓存

  • KV 作为应用层缓存,存储个性化/动态数据;
  • Cloudflare 全局缓存作为CDN 层缓存,存储静态/公共数据;
  • 进一步提升响应速度,降低 KV 读取次数。

使用元数据做数据版本控制:

为键的元数据添加版本号(如 { version: 1, updateTime: Date.now() }),实现数据的版本控制:

  • 读取数据时同时检查版本号,判断是否需要更新;
  • 批量更新时,只需修改版本号元数据,无需遍历所有键。

多命名空间隔离数据:

缓存、配置、会话等不同类型的数据放到不同的命名空间,优势:

  • 数据隔离,避免误操作删除重要数据;
  • 可针对不同命名空间设置不同的清理策略;
  • 便于多环境管理(如开发/生产的缓存命名空间分开)。

KV 开发注意事项

KV 开发注意事项:

严格遵守大小限制:

  • 键最大 512 字节,值最大 25MB,超过会操作失败;
  • 若值超过 25MB,请改用 R2 存储,KV 仅存储 R2 文件的键/URL 作为元数据。

注意数据类型的序列化/反序列化:

KV 仅支持字符串/二进制,存储 JSON/对象/数字时,必须手动:

  • 写入:JSON.stringify(data) 序列化;
  • 读取:JSON.parse(value) 反序列化;
  • 推荐使用 get(key, { type: 'json' }),自动反序列化,简化代码。

KV 的一致性模型:最终一致性:

KV 采用最终一致性,即多区域写入的键值对,同步到全球所有节点需要一定时间(毫秒到秒级)。

  • 适用场景:缓存、配置、非强一致的高频数据;
  • 不适用场景:需要强一致性的场景(如金融交易、实时状态),此类场景请使用 Durable Objects

避免频繁的批量操作:

KV 的批量操作(putMany/deleteMany)虽高效,但单次最大支持 1000 个键,且频繁的大批量操作会消耗写配额,建议:

  • 批量操作按 1000 个键为一组拆分;
  • 非必要不进行全量的批量更新/删除。

注意免费版配额限制:

免费版 KV 有1000 万次读/100 万次写的月配额,超出后会按次计费,建议:

  • 对高频读的接口,增加 Cloudflare 全局缓存,减少 KV 读取次数;
  • 避免循环调用 get/put,尽量使用批量操作;
  • 为缓存数据设置合理的 TTL,减少无效的写操作。

元数据的大小限制:

自定义元数据最大为16KB,且不计入 KV 存储容量,适合存储键的附加信息,无需将附加信息写入值中,减少值的大小。

KV 最佳实践与生态整合

KV 核心适用场景

KV 核心适用场景:

KV 是 Cloudflare 边缘开发的基础存储,适合以下所有高频、小体积、非关联型数据场景:

  • 接口缓存:缓存高频访问的 API 数据,提升响应速度;
  • 系统配置:存储应用的静态配置(如标题、版本、开关),无需修改代码即可更新;
  • 会话管理:存储用户的轻量会话信息(如 token、登录状态),替代传统的 Cookie/Session;
  • 元数据缓存:存储 R2 文件的元数据(URL、大小、类型)、D1 查询结果的缓存,提升查询性能;
  • 临时数据存储:存储验证码、临时令牌等,设置 TTL 自动过期,无需手动清理;
  • 特征标记:存储功能开关、AB 测试配置,实现灰度发布。

Cloudflare 生态整合最佳实践

Cloudflare 生态整合最佳实践:

KV 与 Cloudflare 其他服务深度集成,可实现边缘应用的完整数据闭环,推荐组合:

  • KV + Hono/Workers:实现边缘缓存、配置管理、轻量数据存储,打造低延迟边缘应用;
  • KV + D1:KV 缓存 D1 的查询结果,减少 D1 访问次数,提升接口响应速度;
  • KV + R2:KV 存储 R2 文件的元数据(URL、大小、分类),替代 R2 的 list 方法,提升查询性能;
  • KV + Durable Objects:KV 存储高频读的静态数据,Durable Objects 存储强一致的实时状态,互补使用;
  • KV + Pages:为 Cloudflare Pages 静态站点提供动态配置能力,无需重新部署即可更新站点内容。